<?php
/**
 * Page caching functionality
 *
 * Acknowledgement: The page cache functionality was loosely based on the simple cache plugin - https://github.com/tlovett1/simple-cache
 */

if (!defined('ABSPATH')) die('No direct access allowed');

/**
 * Base cache directory, everything else goes under here
 */
if (!defined('WPO_CACHE_DIR')) define('WPO_CACHE_DIR', untrailingslashit(WP_CONTENT_DIR).'/wpo-cache');

/**
 * Extensions directory.
 */
if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', dirname(__FILE__).'/extensions');

/**
 * Directory that stores config and related files
 */
if (!defined('WPO_CACHE_CONFIG_DIR')) define('WPO_CACHE_CONFIG_DIR', WPO_CACHE_DIR.'/config');

require_once dirname(__FILE__) . '/file-based-page-cache-functions.php';

wpo_cache_load_extensions();

if (!class_exists('WPO_Page_Cache')) :

class WPO_Page_Cache {

	/**
	 * Cache config object
	 *
	 * @var mixed
	 */
	public $config;

	/**
	 * Logger for this class
	 *
	 * @var mixed
	 */
	public $logger;

	/**
	 * File Logger for this class
	 *
	 * @var mixed
	 */
	private $file_logger;

	/**
	 * Instance of this class
	 *
	 * @var WPO_Page_Cache | null
	 */
	public static $instance;

	/**
	 * Store last advanced cache file writing status
	 * If true then last writing finished with error
	 *
	 * @var bool
	 */
	public $advanced_cache_file_writing_error;

	/**
	 * Store errors
	 *
	 * @var array
	 */
	private $_errors = array();

	/**
	 * Last advanced cache file content
	 *
	 * @var string
	 */
	public $advanced_cache_file_content;

	/**
	 * Store the latest advanced-cache.php version required
	 *
	 * @var string
	 */
	private $_minimum_advanced_cache_file_version = '3.0.17';

	/**
	 * Flag to decide whether to purge or not
	 *
	 * @var bool
	 */
	public $should_purge;

	/**
	 * Flag to decide whether to purge or not
	 *
	 * @var bool
	 */
	private $files_to_ignore = array('index.php','.htaccess');

	/**
	 * Set everything up here
	 */
	public function __construct() {
		$this->config = WPO_Cache_Config::instance();
		if (empty($GLOBALS['wpo_cache_config'])) {
			$config_file_path = $this->config->get_config_file_path();
			if (file_exists($config_file_path)) {
				include_once($config_file_path);
			}
		}

		WPO_Cache_Rules::instance();
		WP_Optimize_Page_Cache_Preloader::instance();

		$this->logger = new Updraft_PHP_Logger();
		$this->file_logger = new Updraft_File_Logger(WP_Optimize_Utils::get_log_file_path('cache'));

		add_action('activated_plugin', array($this, 'activate_deactivate_plugin'));
		add_action('deactivate_plugin', array($this, 'activate_deactivate_plugin'));
		add_action('wpo_purge_old_cache', array($this, 'purge_old'));
		add_action('wpo_prune_cache_logs', array($this, 'prune_cache_logs'));

		/**
		 * Regenerate config file on cache flush.
		 */
		add_action('wpo_cache_flush', array($this, 'update_cache_config'));
		add_action('wpo_cache_flush', array($this, 'delete_cache_size_information'));
		add_action('update_option_permalink_structure', array($this, 'update_option_permalink_structure'), 10, 3);

		// Add purge cache link to admin bar.
		add_filter('wpo_cache_admin_bar_menu_items', array($this, 'admin_bar_purge_cache'), 20, 1);

		// Handle single page purge.
		add_action('wp_loaded', array($this, 'handle_purge_single_page_cache'));

		add_action('admin_init', array($this, 'admin_init'));

		add_action('update_option_gmt_offset', array($this, 'update_gmt_offset_timezone_string_config'), 10, 3);
		add_action('update_option_timezone_string', array($this, 'update_gmt_offset_timezone_string_config'), 10, 3);
		add_action('update_option_date_format', array($this, 'update_option_date_format'), 10, 2);
		add_action('update_option_time_format', array($this, 'update_option_time_format'), 10, 2);

		$this->check_compatibility_issues();

		$this->ensure_cron_schedules_filter_registered();

		add_action('wpo_save_images_settings', array($this, 'update_webp_images_option'));

		add_action('wpo_preload_url', array($this, 'maybe_preload_url'));

		// Setup filters for exceptions.
		add_filter('wpo_restricted_cache_page_type', 'wpo_restricted_cache_page_type');
		add_filter('wpo_url_in_conditional_tags_exceptions', 'wpo_url_in_conditional_tags_exceptions');
	}

	/**
	 * Ensures that the cron-related filter is registered.
	 *
	 * If `init` has already run, the filter is registered immediately.
	 * Otherwise, it schedules the registration to occur during `init`.
	 *
	 * @return void
	 */
	private function ensure_cron_schedules_filter_registered() {
		if (did_action('init')) {
			$this->register_cron_schedules_filter();
		} else {
			add_action('init', array($this, 'register_cron_schedules_filter'));
		}
	}

	/**
	 * Registers the custom cron schedule filter for WP-Cron.
	 *
	 * This method hooks the class's `cron_schedules()` method into WordPress's
	 * `cron_schedules` filter, allowing the plugin to define custom cron intervals.
	 *
	 * It is recommended to call this method on or after the `init` action to ensure
	 * that translations and other dependencies are properly loaded before the filter runs.
	 *
	 * @return void
	 */
	public function register_cron_schedules_filter() {
		add_filter('cron_schedules', array($this, 'cron_schedules'));
	}

	/**
	 * Determines whether the current page should be cached.
	 *
	 * This method checks cache rules to identify if caching is possible.
	 * If caching is not allowed, it adds appropriate HTTP headers and debug messages.
	 *
	 * @return bool True if the page should be cached, false otherwise.
	 */
	public function should_cache_page() {

		if (!$this->is_enabled()) return false;

		$no_cache_because = array();

		// Check serve cache rules, to identify if we need to cache the page.
		$can_serve_from_cache = wpo_can_serve_from_cache();
		
		if (false === $can_serve_from_cache) return false;

		if (is_array($can_serve_from_cache)) $no_cache_because = $can_serve_from_cache;

		if (!empty($no_cache_because)) {
			do_action('wpo_page_not_cached', $no_cache_because);

			// Add http header
			if (!wp_doing_cron()) {
				$no_cache_because_message = join(", ", $no_cache_because);
				wpo_cache_add_nocache_http_header_with_send_headers_action($no_cache_because_message);

				$not_cached_details = "";
				
				// Output the reason only when the user has turned on debugging
				if ((defined('WP_DEBUG') && WP_DEBUG) || !empty(TeamUpdraft\WP_Optimize\Includes\Fragments\fetch_superglobal('get', 'wpo_cache_debug'))) {
					$not_cached_details = "because: ".$no_cache_because_message;
				}
				wpo_cache_add_footer_output(sprintf("Page not served from cache %s", $not_cached_details));
			}

			return false;
		}

		return true;
	}

	/**
	 * Activate cron job when the cache is enabled for deleting expired cache files
	 */
	public function cron_activate() {
		$page_cache_length = $this->config->get_option('page_cache_length');
		if ($this->is_enabled()) {
			if (!wp_next_scheduled('wpo_purge_old_cache')) {
				wp_schedule_event(time() + (false === $page_cache_length ? '86400' : $page_cache_length), 'wpo_purge_old_cache', 'wpo_purge_old_cache');
			}
			if (!wp_next_scheduled('wpo_prune_cache_logs')) {
				wp_schedule_event(time(), 'weekly', 'wpo_prune_cache_logs');
			}
		} else {
			wp_clear_scheduled_hook('wpo_purge_old_cache');
			wp_clear_scheduled_hook('wpo_prune_cache_logs');
		}
	}

	/**
	 * Do required actions on activate/deactivate any plugin.
	 */
	public function activate_deactivate_plugin() {

		wp_clear_scheduled_hook('wpo_purge_old_cache');

		$this->update_cache_config();

		$is_cache_purged = $this->purge();

		if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: plugin activation/deactivation");
	}

	/**
	 * Check if current user can purge cache.
	 *
	 * @return bool
	 */
	public function can_purge_cache() {
		if (!$this->is_enabled()) return false;
		$required_capability = is_multisite() ? 'manage_network_options' : 'manage_options';

		if (WP_Optimize::is_premium()) {
			return current_user_can($required_capability) || WP_Optimize_Premium()->can_purge_the_cache();
		} else {
			return current_user_can($required_capability);
		}
	}

	/**
	 * Add Purge from cache in admin bar.
	 *
	 * @param array        $menu_items
	 * @return array
	 */
	public function admin_bar_purge_cache($menu_items) {
		global $pagenow;
		if (!$this->can_purge_cache()) return $menu_items;

		$act_url = remove_query_arg(array('wpo_single_page_cache_purged', 'wpo_all_pages_cache_purged'));

		$cache_size = $this->get_cache_size();
		$cache_size_info = '<h4>'.__('Page cache', 'wp-optimize').'</h4>';
		// translators: %d is the number of files in the cache
		$cache_size_info .= '<span>'.__('Cache size:', 'wp-optimize').' '. WP_Optimize()->format_size($cache_size['size']).' '.sprintf(__('(%d files)', 'wp-optimize'), $cache_size['file_count']).'</span>';

		$menu_items[] = array(
			'id'    => 'wpo_cache_stats',
			'title' => $cache_size_info,
			'meta'  => array(
				'class' => 'wpo-cache-stats',
			),
			'parent' => 'wpo_purge_cache',
		);

		$menu_items[] = array(
			'id'    => 'wpo_purge_all_pages_cache',
			'title' => __('Purge cache for all pages', 'wp-optimize'),
			'href'  => add_query_arg('_wpo_purge', wp_create_nonce('wpo_purge_all_pages_cache'), $act_url),
			'meta'  => array(
				'title' => __('Purge cache for all pages', 'wp-optimize'),
			),
			'parent' => 'wpo_purge_cache',
		);

		if (!is_admin() || 'post.php' === $pagenow) {
			$menu_items[] = array(
				'id'    => 'wpo_purge_this_page_cache',
				'title' => __('Purge cache for this page', 'wp-optimize'),
				'href'  => add_query_arg('_wpo_purge', wp_create_nonce('wpo_purge_single_page_cache'), $act_url),
				'meta'  => array(
					'title' => __('Purge cache for this page', 'wp-optimize'),
				),
				'parent' => 'wpo_purge_cache',
			);
		}

		return $menu_items;

	}

	/**
	 * Check if purge single page action sent and purge cache.
	 */
	public function handle_purge_single_page_cache() {

		if (!$this->can_purge_cache()) return;

		if (isset($_GET['wpo_single_page_cache_purged']) || isset($_GET['wpo_all_pages_cache_purged'])) {
			// phpcs:disable
			// We are not using $_GET values, just checks if the variable is set and then use hard coded string
			if (isset($_GET['wpo_single_page_cache_purged'])) {
				$notice_function = $_GET['wpo_single_page_cache_purged'] ? 'notice_purge_single_page_cache_success' : 'notice_purge_single_page_cache_error';
			} else {
				$notice_function = $_GET['wpo_all_pages_cache_purged'] ? 'notice_purge_all_pages_cache_success' : 'notice_purge_all_pages_cache_error';
			}
			// phpcs:enable

			add_action('admin_notices', array($this, $notice_function));

			return;
		}

		if (!isset($_GET['_wpo_purge'])) return;

		if (wp_verify_nonce(sanitize_key($_GET['_wpo_purge']), 'wpo_purge_single_page_cache')) {
			$success = false;

			if (is_admin()) {
				$post = isset($_GET['post']) ? (int) $_GET['post'] : 0;
				if ($post > 0) {
					$success = self::delete_single_post_cache($post);
					if ($success) $this->file_log("Cache for URL: {{URL}} has been purged, triggered by: " . __METHOD__, $post);
				}
			} else {
				$url = wpo_current_url();
				$success = self::delete_cache_by_url($url);
				if ($success) $this->file_log("Cache for URL: " . self::remove_query_params($url) . " has been purged, triggered by: " . __METHOD__);
			}

			// remove nonce from url and reload page.
			wp_redirect(add_query_arg('wpo_single_page_cache_purged', $success, remove_query_arg('_wpo_purge')));
			exit;

		} elseif (wp_verify_nonce(sanitize_key($_GET['_wpo_purge']), 'wpo_purge_all_pages_cache')) {
			$success = self::purge();
			$this->maybe_set_preload_cron_job();
			if ($success) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);

			// remove nonce from url and reload page.
			wp_redirect(add_query_arg('wpo_all_pages_cache_purged', $success, remove_query_arg('_wpo_purge')));
			exit;
		}
	}

	/**
	 * Sets a preload cron job to run after 30 seconds
	 *
	 * As preloading may take time for larger sites, we are opting for an immediate cron job instead
	 * of instant preloading to prevent the current request from slowing down the page load
	 *
	 * @return void
	 */
	private function maybe_set_preload_cron_job() {
		$wpo_page_cache_preloader = WP_Optimize_Page_Cache_Preloader::instance();

		if ($wpo_page_cache_preloader->is_scheduled_preload_enabled() || $this->should_auto_preload_purged_contents()) {
			if (!wp_next_scheduled('wpo_page_cache_run_preload')) {
				wp_schedule_single_event(time() + 30, 'wpo_page_cache_run_preload');
			}
		}
	}

	/**
	 * Show notification when page cache purged successfully.
	 */
	public function notice_purge_single_page_cache_success() {
		$this->show_notice(__('The page cache was successfully purged.', 'wp-optimize'), 'success');
	}

	/**
	 * Show notification when page cache wasn't purged.
	 */
	public function notice_purge_single_page_cache_error() {
		$this->show_notice(__('The page cache was not purged.', 'wp-optimize'), 'error');
	}

	/**
	 * Show notification when all pages cache purged successfully.
	 */
	public function notice_purge_all_pages_cache_success() {
		$this->show_notice(__('The page cache was successfully purged.', 'wp-optimize'), 'success');
	}

	/**
	 * Show notification when all pages cache wasn't purged.
	 */
	public function notice_purge_all_pages_cache_error() {
		$this->show_notice(__('The page cache was not purged.', 'wp-optimize'), 'error');
	}

	/**
	 * Show notification in WordPress admin.
	 *
	 * @param string $message HTML (no further escaping is performed)
	 * @param string $type    error, warning, success, or info
	 */
	public function show_notice($message, $type) {
		global $current_screen;
		
		if ($current_screen && is_callable(array($current_screen, 'is_block_editor')) && $current_screen->is_block_editor()) : ?>
			<script>
				window.addEventListener('load', function() {
					(function(wp) {
						if (window.wp && wp.hasOwnProperty('data') && 'function' == typeof wp.data.dispatch) {
							wp.data.dispatch('core/notices').createNotice(
								'<?php echo esc_html($type); ?>',
								'<?php echo wp_kses_post($message); ?>',
								{
									isDismissible: true,
								}
							);
						}
					})(window.wp);
				});
			</script>
		<?php else : ?>
			<div class="notice wpo-notice notice-<?php echo esc_attr($type); ?> is-dismissible">
				<p><?php echo wp_kses_post($message); ?></p>
			</div>
		<?php
		endif;
	}

	/**
	 * Enables page cache
	 *
	 * @param bool $force_enable - Force regenerating everything. E.g. we want to do that when saving the settings
	 *
	 * @return WP_Error|bool - true on success, error otherwise
	 */
	public function enable($force_enable = false) {
		global $is_apache;
		static $already_ran_enable = false;

		if ($already_ran_enable) return $already_ran_enable;

		$folders_created = $this->create_folders();
		if (is_wp_error($folders_created)) {
			$already_ran_enable = $folders_created;
			return $already_ran_enable;
		}

		if (!file_exists($this->config->get_config_file_path())) {
			$result = $this->update_cache_config();
			if (is_wp_error($result)) return $result;
		}

		// if WPO_ADVANCED_CACHE isn't set, or environment doesn't contain the right constant, force regeneration
		if (!defined('WPO_ADVANCED_CACHE') || !defined('WP_CACHE')) {
			$force_enable = true;
		}

		$this->maybe_regenerate_cache_config_file();

		// Enable `.htaccess` rules to serve cached files
		if ($is_apache && apply_filters('wpo_serve_cache_via_htaccess', false)) {
			wpo_disable_cache_directories_viewing();
			wpo_allow_access_to_index_cache_files();
			$this->update_serve_cache_rules_htaccess_section(true);
		}

		if (!$force_enable) {
			$already_ran_enable = true;
			return true;
		}

		if (!$this->write_advanced_cache() && version_compare($this->get_advanced_cache_version(), $this->_minimum_advanced_cache_file_version, '<')) {
			$message = sprintf("The request to write the file %s failed. ", htmlspecialchars($this->get_advanced_cache_filename()));
			$message .= ' '.__('Please check file and directory permissions on the file paths up to this point, and your PHP error log.', 'wp-optimize');

			if (!defined('WP_CLI') || !WP_CLI) {
				// translators: %s is the path to advanced-cache.php folder
				$message .= "\n\n1. ".sprintf(__('Please navigate, via FTP, to the folder - %s', 'wp-optimize'), htmlspecialchars(dirname($this->get_advanced_cache_filename())));
				$message .= "\n2. ".__('Edit or create a file with the name advanced-cache.php', 'wp-optimize');
				$message .= "\n3. ".__('Copy and paste the following lines into the file:', 'wp-optimize');
			}

			$already_ran_enable = new WP_Error("write_advanced_cache", $message);
			return $already_ran_enable;
		}

		if (!$this->write_wp_config(true)) {
			$already_ran_enable = new WP_Error("write_wp_config", "Could not turn on the WP_CACHE constant in wp-config.php. Check your permissions.");
			return $already_ran_enable;
		}

		if (!$this->verify_cache()) {
			$errors = $this->get_errors();
			$already_ran_enable = new WP_Error("verify_cache", "Could not verify if the cache was enabled: \n".implode("\n- ", $errors));
			return $already_ran_enable;
		}

		$already_ran_enable = true;

		return true;
	}

	/**
	 * Disables page cache
	 *
	 * @return bool - true on success, false otherwise
	 */
	public function disable() {
		global $is_apache;

		$ret = true;

		$advanced_cache_file = $this->get_advanced_cache_filename();

		// N.B. The only use of WP_CACHE in WP core is to include('advanced-cache.php') (and run a function if it's then defined); so, if the decision to leave it enable is, for some unexpected reason, technically incorrect, it still can't cause a problem.
		$disabled_wp_config = $this->write_wp_config(false);
		if (!$disabled_wp_config) {
			$plugin_basename = basename(WPO_PLUGIN_MAIN_PATH);
			$action = "deactivate_".$plugin_basename."/wp-optimize.php";
			if (current_action() === $action) {
				$cache_config = WPO_Cache_Config::instance()->config;
				$cache_config['enable_page_caching'] = false;
				WPO_Cache_Config::instance()->update($cache_config, true);
			}
			$this->log("Could not turn off the WP_CACHE constant in wp-config.php");
			$this->add_warning('error_disabling', __('Could not turn off the WP_CACHE constant in wp-config.php', 'wp-optimize'));
		}

		$disabled_advanced_cache = true;
		// First try to remove (so that it doesn't look to any other plugin like the file is already 'claimed')
		// We only touch advanched-cache.php and wp-config.php if it appears that we were in control of advanced-cache.php
		if (!file_exists($advanced_cache_file) || false !== strpos(file_get_contents($advanced_cache_file), 'WP-Optimize advanced-cache.php')) {
			if (file_exists($advanced_cache_file) && (!wp_delete_file($advanced_cache_file) && false === file_put_contents($advanced_cache_file, "<?php\n// WP-Optimize: page cache disabled"))) {
				$disabled_advanced_cache = false;
				$this->log("The request to the filesystem to remove or empty advanced-cache.php failed");
				$this->add_warning('error_disabling', __('The request to the filesystem to remove or empty advanced-cache.php failed', 'wp-optimize'));
			}
		}

		// If both actions failed, the cache wasn't disabled. So we send an error. If only one succeeds, it will still be disabled.
		if (!$disabled_wp_config && !$disabled_advanced_cache) {
			$ret = new WP_Error('error_disabling_cache', __('The page caching could not be disabled: the WP_CACHE constant could not be removed from wp-config.php and the request to the filesystem to remove or empty advanced-cache.php failed.', 'wp-optimize'));
		}

		// Delete cache to avoid stale cache on next activation
		$is_cache_purged = $this->purge();
		if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);

		// Delete `.htaccess` rules to serve cached files
		if ($is_apache && apply_filters('wpo_serve_cache_via_htaccess', false)) {
			$this->update_serve_cache_rules_htaccess_section(false);
		}
		
		// Unschedule cron job to purge old cache
		wp_clear_scheduled_hook('wpo_purge_old_cache');

		if (!is_wp_error($ret)) wp_clear_scheduled_hook('wpo_prune_cache_logs');

		return $ret;
	}


	/**
	 * Purges the cache
	 *
	 * @return bool - true on success, false otherwise
	 */
	public function purge() {
		/**
		 * Filters whether activating / deactivating a plugin will purge the cache.
		 */
		$this->should_purge = apply_filters('wpo_purge_page_cache_on_activate_deactivate_plugin', true);
		$purge_actions = array(
			'deactivate_' . plugin_basename(WPO_PLUGIN_MAIN_PATH . 'wp-optimize.php'),
			'deactivate_plugin',
			'activated_plugin',
		);
		$is_deactivation_action = in_array(current_action(), $purge_actions, true);
		if ($is_deactivation_action && !$this->should_purge) return false;

		if (!self::delete(WPO_CACHE_FILES_DIR)) {
			$this->log("The request to the filesystem to delete the cache failed");
			return false;
		}

		/**
		 * Fires after purging the cache
		 */
		do_action('wpo_cache_flush');

		return true;
	}

	/**
	 * Purge cache files older than cache life span
	 *
	 * @return array Log messages.
	 */
	public function purge_old() {
		$log = array();
		$page_cache_length = $this->config->get_option('page_cache_length');
		if (0 === $page_cache_length) return $log;
		$expires = time() - $page_cache_length;
		$cache_folder = WPO_CACHE_FILES_DIR . '/' . str_ireplace(array('http://', 'https://'), '', get_site_url());
		// get all directories that are a direct child of current directory
		if (is_dir($cache_folder) && wp_is_writable($cache_folder)) {
			if ($handle = opendir($cache_folder)) {
				while (false !== ($d = readdir($handle))) {
					if (0 === strcmp($d, '.') || 0 === strcmp($d, '..')) {
						continue;
					}
					
					if ($this->is_front_page_cache($d)) {
						$modified_time = (int) filemtime("$cache_folder/$d");
						if ($modified_time <= $expires) {
							wp_delete_file("$cache_folder/$d");
						}
						continue;
					}

					$dir = $cache_folder.'/'.$d;
					if (!is_dir($dir)) continue;

					$stat = stat($dir);
					$modified_time = $stat['mtime'];
					$log[] = "checking if cache has expired - $d";
					if ($modified_time <= $expires) {
						$log[] = "deleting cache in $dir";
						wpo_delete_files($dir, true);
						if (file_exists($dir)) rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- N/A
					}
				}
				closedir($handle);
			}
		}
		return $log;
	}

	/**
	 * Finds out given cache file is of front page or not
	 *
	 * @return bool
	 */
	private function is_front_page_cache($d) {
		return in_array($d, array('index.html', 'index.html.gz'));
	}

	/**
	 * Purges the cache
	 *
	 * @return bool - true on success, false otherwise
	 */
	public function clean_up() {

		$this->disable();

		if (!self::delete(WPO_CACHE_DIR, true)) {
			$this->log("The request to the filesystem to clean up the cache failed");
			return false;
		}

		return true;
	}

	/**
	 * Check if cache is enabled and working
	 *
	 * @return bool - true on success, false otherwise
	 */
	public function is_enabled() {

		if (!$this->config->get_option('enable_page_caching')) return false;

		if (!defined('WP_CACHE') || !WP_CACHE) {
			if ((!(defined('WP_CLI') && WP_CLI)) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
				$this->log("WP_CACHE constant is not present in wp-config.php");
				return false;
			}
		}

		if (!defined('WPO_ADVANCED_CACHE') || !WPO_ADVANCED_CACHE) {
			if ((!(defined('WP_CLI') && WP_CLI)) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
				$this->log("WPO_ADVANCED_CACHE constant is not present in advanced-cache.php");
				return false;
			}
		}

		$config_file = WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename();
		if (!file_exists($config_file)) {
			if ((!(defined('WP_CLI') && WP_CLI)) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
				$this->log("Cache config file $config_file is not present");
				return false;
			}
		}

		return true;
	}

	/**
	 * Create the folder structure needed for cache to work
	 *
	 * @return bool | WP_Error - true on success, false otherwise
	 */
	public function create_folders() {
		$permissions_str = __('Please check your file permissions.', 'wp-optimize');

		if (!is_dir(WPO_CACHE_DIR) && !wp_mkdir_p(WPO_CACHE_DIR)) {
			// translators: %s is a directory name
			return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s.', 'wp-optimize'), str_ireplace(ABSPATH, '', WPO_CACHE_DIR)) .' '. $permissions_str);
		}

		if (!is_dir(WPO_CACHE_CONFIG_DIR) && !wp_mkdir_p(WPO_CACHE_CONFIG_DIR)) {
			// translators: %s is a directory name
			return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s.', 'wp-optimize'), str_ireplace(ABSPATH, '', WPO_CACHE_CONFIG_DIR)) .' '. $permissions_str);
		}
		
		if (!is_dir(WPO_CACHE_FILES_DIR)) {
			if (!wp_mkdir_p(WPO_CACHE_FILES_DIR)) {
				// translators: %s is a directory name
				return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s.', 'wp-optimize'), str_ireplace(ABSPATH, '', WPO_CACHE_FILES_DIR)) .' '. $permissions_str);
			} else {
				wpo_disable_cache_directories_viewing();
			}
		}

		return true;
	}

	/**
	 * Get advanced-cache.php file name with full path.
	 *
	 * @return string
	 */
	public function get_advanced_cache_filename() {
		return untrailingslashit(WP_CONTENT_DIR) . '/advanced-cache.php';
	}

	/**
	 * Writes advanced-cache.php
	 *
	 * @param boolean $update_required - Whether the update is required or not.
	 * @return bool
	 */
	private function write_advanced_cache($update_required = false) {
		$config_file_basename = $this->config->get_cache_config_filename();
		$cache_file_basename = untrailingslashit(plugin_dir_path(__FILE__));
		$plugin_basename = basename(WPO_PLUGIN_MAIN_PATH);
		$cache_path = '/wpo-cache';
		$cache_files_path = '/cache/wpo-cache';
		$cache_extensions_path = WPO_CACHE_EXT_DIR;
		$wpo_version = WPO_VERSION;
		$wpo_home_url = trailingslashit(network_home_url());
		$abspath = ABSPATH;

		// CS does not like heredoc
		// phpcs:disable
		$this->advanced_cache_file_content = <<<EOF
<?php

if (!defined('ABSPATH')) die('No direct access allowed');

// WP-Optimize advanced-cache.php (written by version: $wpo_version) (homeurl: $wpo_home_url) (abspath: $abspath) (do not change this line, it is used for correctness checks)

if (!defined('WPO_ADVANCED_CACHE')) define('WPO_ADVANCED_CACHE', true);

\$possible_plugin_locations = array(
	defined('WP_PLUGIN_DIR') ? WP_PLUGIN_DIR.'/$plugin_basename/cache' : false,
	defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR.'/plugins/$plugin_basename/cache' : false,
	dirname(__FILE__).'/plugins/$plugin_basename/cache',
	'$cache_file_basename',
);

\$plugin_location = false;

foreach (\$possible_plugin_locations as \$possible_location) {
	if (false !== \$possible_location && @file_exists(\$possible_location.'/file-based-page-cache.php')) {
		\$plugin_location = \$possible_location;
		break;
	}
}

if (false === \$plugin_location) {
	if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND')) define('WPO_PLUGIN_LOCATION_NOT_FOUND', true);
	\$protocol = \$_SERVER['REQUEST_SCHEME'];
	\$host = \$_SERVER['HTTP_HOST'];
	\$request_uri = \$_SERVER['REQUEST_URI'];
	if (strcasecmp('$wpo_home_url', \$protocol . '://' . \$host . \$request_uri) === 0) {
		error_log('WP-Optimize: No caching took place, because the plugin location could not be found');
	}
} else {
	if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND')) define('WPO_PLUGIN_LOCATION_NOT_FOUND', false);
}

if (is_admin()) { return; }

if (!defined('WPO_CACHE_DIR')) define('WPO_CACHE_DIR', WP_CONTENT_DIR.'$cache_path');
if (!defined('WPO_CACHE_CONFIG_DIR')) define('WPO_CACHE_CONFIG_DIR', WPO_CACHE_DIR.'/config');
if (!defined('WPO_CACHE_FILES_DIR')) define('WPO_CACHE_FILES_DIR', WP_CONTENT_DIR.'$cache_files_path');
if (false !== \$plugin_location) {
	if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', \$plugin_location.'/extensions');
} else {
	if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', '$cache_extensions_path');
}

if (!@file_exists(WPO_CACHE_CONFIG_DIR . '/$config_file_basename')) { return; }

\$GLOBALS['wpo_cache_config'] = @json_decode(file_get_contents(WPO_CACHE_CONFIG_DIR . '/$config_file_basename'), true);

if (empty(\$GLOBALS['wpo_cache_config'])) {
	include_once(WPO_CACHE_CONFIG_DIR . '/$config_file_basename');
}

if (empty(\$GLOBALS['wpo_cache_config'])) {
	error_log('WP-Optimize: Caching failed because the configuration data could not be loaded from the config file.');
	return;
}

if (empty(\$GLOBALS['wpo_cache_config']['enable_page_caching'])) { return; }

if (false !== \$plugin_location) { include_once(\$plugin_location.'/file-based-page-cache.php'); }

EOF;

		// phpcs:enable
		$advanced_cache_filename = $this->get_advanced_cache_filename();

		// If the file content is already up to date, success
		if (is_file($advanced_cache_filename) && file_get_contents($advanced_cache_filename) === $this->advanced_cache_file_content) {
			$this->advanced_cache_file_writing_error = false;
			return true;
		}

		// check if we can't write the advanced cache file
		// case 1: the directory is read-only and the file doesn't exist
		if (!is_file($advanced_cache_filename) && !wp_is_writable(dirname($advanced_cache_filename))) {
			$this->advanced_cache_file_writing_error = true;
			return false;
		}

		// case 2: the file already exists but it's read-only
		if (is_file($advanced_cache_filename) && !wp_is_writable($advanced_cache_filename)) {
			if (version_compare($this->get_advanced_cache_version(), $this->_minimum_advanced_cache_file_version, '<') || $update_required) {
				$this->advanced_cache_file_writing_error = true;
				return false;
			} else {
				$this->advanced_cache_file_writing_error = false;
				return true;
			}
		}

		if (!file_put_contents($this->get_advanced_cache_filename(), $this->advanced_cache_file_content)) {
			$this->advanced_cache_file_writing_error = true;
			return false;
		}

		$this->advanced_cache_file_writing_error = false;
		return true;
	}

	/**
	 * Update advanced cache version if needed.
	 */
	public function maybe_update_advanced_cache() {

		if (!$this->is_enabled()) return;

		if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND') || (defined('WPO_PLUGIN_LOCATION_NOT_FOUND') && true === WPO_PLUGIN_LOCATION_NOT_FOUND)) {
			if (!$this->write_advanced_cache(true)) {
				add_action('admin_notices', array($this, 'show_admin_notice_advanced_cache'));
			}
		}

		// from 3.0.17 we use more secure way to store cache config files and need update advanced-cache.php
		// if the site url or folder changed we also update advanced-cache.php
		$advanced_cache_current_version = $this->get_advanced_cache_version();
		$is_the_site_url_or_folder_changed = $this->is_the_site_url_or_folder_changed();

		if ($advanced_cache_current_version && version_compare($advanced_cache_current_version, $this->_minimum_advanced_cache_file_version, '>=') && !$is_the_site_url_or_folder_changed) return;

		// Purge the cache when the site url or folder changed.
		if ($is_the_site_url_or_folder_changed) {
			$is_cache_purged = $this->purge();
			if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
		}

		if (!$this->write_advanced_cache()) {
			add_action('admin_notices', array($this, 'notice_advanced_cache_autoupdate_error'));
		} else {
			$this->update_cache_config();
		}
	}

	/**
	 * Check if the site url or folder doesn't equal to values from advanced-cache.php
	 *
	 * @return bool
	 */
	private function is_the_site_url_or_folder_changed() {
		if (!is_file($this->get_advanced_cache_filename())) return false;

		$content = file_get_contents($this->get_advanced_cache_filename());
		
		if (false === $content) return false;

		if (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\) (\(homeurl: (.+)\)) (\(abspath: (.+)\)) \(do not change/Ui', $content, $match)) {
			$wpo_home_url = trailingslashit(network_home_url());
			$abspath = ABSPATH;
			return ($wpo_home_url !== $match[3] || $abspath !== $match[5]);
		} elseif (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\)/Ui', $content, $match)) {
			return true;
		}

		return false;
	}

	/**
	 * Show notification when advanced-cache.php could not be updated.
	 */
	public function notice_advanced_cache_autoupdate_error() {
		$this->show_notice(__('The file advanced-cache.php needs to be updated, but the automatic process failed.', 'wp-optimize').
		' <a href="'.admin_url('admin.php?page=wpo_cache').'">'.__('Please try to disable and then re-enable the WP-Optimize cache manually.', 'wp-optimize').'</a>', 'error');
	}

	/**
	 * Get WPO version number from advanced-cache.php file.
	 *
	 * @return bool|mixed
	 */
	public function get_advanced_cache_version() {
		if (!is_file($this->get_advanced_cache_filename())) return false;

		$version = false;
		$content = file_get_contents($this->get_advanced_cache_filename());

		if (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\)/Ui', $content, $match)) {
			$version = $match[1];
		}

		return $version;
	}

	/**
	 * Set WP_CACHE on or off in wp-config.php
	 *
	 * @param  boolean $status value of WP_CACHE.
	 * @return boolean true if the value was set, false otherwise
	 */
	private function write_wp_config($status = true) {
		// If we changed the value in wp-config, save it, in case we need to change it again in the same run.
		static $changed = false;

		if (defined('WP_CACHE') && WP_CACHE === $status && !$changed) {
			return true;
		}

		$config_path = $this->_get_wp_config();

		// Couldn't find wp-config.php.
		if (!$config_path) {
			return false;
		}

		$config_file_string = file_get_contents($config_path);

		// Config file is empty. Maybe couldn't read it?
		if (empty($config_file_string)) {
			return false;
		}

		$config_file = preg_split("#(\n|\r\n)#", $config_file_string);
		$line_key    = false;

		foreach ($config_file as $key => $line) {
			if (!preg_match('/^\s*define\(\s*(\'|")([A-Z_]+)(\'|")(.*)/i', $line, $match)) {
				continue;
			}

			if ('WP_CACHE' === $match[2]) {
				$line_key = $key;
			}
		}

		if (false !== $line_key) {
			unset($config_file[$line_key]);
		}


		if ($status) {
			array_shift($config_file);
			array_unshift($config_file, '<?php', "define('WP_CACHE', true); // WP-Optimize Cache");
		}

		foreach ($config_file as $key => $line) {
			if ('' === $line) {
				unset($config_file[$key]);
			}
		}
		if (!file_put_contents($config_path, implode(PHP_EOL, $config_file))) {
			return false;
		}
		$changed = true;
		return true;
	}

	/**
	 * Verify we can write to the file system
	 *
	 * @return boolean
	 */
	private function verify_cache() {
		if (function_exists('clearstatcache')) {
			clearstatcache();
		}
		$errors = 0;

		// First check wp-config.php.
		if (!$this->_get_wp_config() && !wp_is_writable($this->_get_wp_config())) {
			$this->log("Unable to write to or find wp-config.php; please check file/folder permissions");
			$this->add_warning('verify_cache', __("Unable to write to or find wp-config.php; please check file/folder permissions.", 'wp-optimize'));
		}

		$advanced_cache_file = untrailingslashit(WP_CONTENT_DIR).'/advanced-cache.php';
		
		// Now check wp-content. We need to be able to create files of the same user as this file.
		if ((!file_exists($advanced_cache_file) || false === strpos(file_get_contents($advanced_cache_file), 'WP-Optimize advanced-cache.php')) && !wp_is_writable($advanced_cache_file) && !wp_is_writable(untrailingslashit(WP_CONTENT_DIR))) {
			$this->log("Unable to write the file advanced-cache.php inside the wp-content folder; please check file/folder permissions");
			$this->add_error('verify_cache', __("Unable to write the file advanced-cache.php inside the wp-content folder; please check file/folder permissions", 'wp-optimize'));
			$errors++;
		}

		if (file_exists(WPO_CACHE_FILES_DIR)) {
			if (!wp_is_writable(WPO_CACHE_FILES_DIR)) {
				$this->log("Unable to write inside the cache files folder; please check file/folder permissions");
				// translators: %s is cache folder path
				$this->add_warning('verify_cache', sprintf(__("Unable to write inside the cache files folder (%s); please check file/folder permissions (no cache files will be able to be created otherwise)", 'wp-optimize'), WPO_CACHE_FILES_DIR));
			}
		}
		
		if (file_exists(WPO_CACHE_CONFIG_DIR)) {
			if (!wp_is_writable(WPO_CACHE_CONFIG_DIR)) {
				$this->log("Unable to write inside the cache configuration folder; please check file/folder permissions");
				// If the config exists, only send a warning. Otherwise send an error.
				$type = 'warning';
				if (!file_exists(WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename())) {
					$type = 'error';
					$errors++;
				}
				// translators: %s is cache config folder path
				$this->add_error('verify_cache', sprintf(__("Unable to write inside the cache configuration folder (%s); please check file/folder permissions", 'wp-optimize'), WPO_CACHE_CONFIG_DIR), $type);
			}
		}

		return !$errors;
	}

	/**
	 * Update permalink structure in cache config
	 *
	 * @param string $old_value Old value of permalink_structure option
	 * @param string $value 	New value of permalink_structure option
	 * @param string $option 	Option name `permalink_structure`
	 */
	public function update_option_permalink_structure($old_value, $value, $option) {
		$current_config = $this->config->get();
		if ($old_value !== $value) {
			$current_config[$option] = $value;
			$this->config->update($current_config, true);
		}
	}

	/**
	 * Update cache config. Used to support 3d party plugins.
	 *
	 * @return bool | WP_Error
	 */
	public function update_cache_config() {
		// get current cache settings.
		$current_config = $this->config->get();
		// and call update to change if need cookies and query variable names.
		return $this->config->update($current_config);
	}

	/**
	 * Delete information about cache size.
	 */
	public function delete_cache_size_information() {
		//not removing delete_transient so it clear this key from option table
		delete_transient('wpo_get_cache_size');
		delete_site_transient('wpo_get_cache_size');
	}

	/**
	 * Get current cache size.
	 *
	 * @return array
	 */
	public function get_cache_size() {
		$cache_size = get_site_transient('wpo_get_cache_size');

		if (!empty($cache_size)) return $cache_size;

		$infos = $this->get_dir_infos(WPO_CACHE_FILES_DIR);
		$cache_size = array(
			'size' => $infos['size'],
			'file_count' => $infos['file_count']
		);

		$expiration = apply_filters('wpo_cache_size_expiration', DAY_IN_SECONDS);
		$expiration = is_numeric($expiration) ? (int) $expiration : DAY_IN_SECONDS;
		set_site_transient('wpo_get_cache_size', $cache_size, $expiration);

		return $cache_size;
	}

	/**
	 * Fetch directory information.
	 *
	 * @param string $dir
	 * @return array
	 */
	private function get_dir_infos($dir) {
		$dir_size = 0;
		$file_count = 0;

		$handle = is_dir($dir) ? opendir($dir) : false;

		if (false === $handle) {
			return array('size' => 0, 'file_count' => 0);
		}

		$filtered = apply_filters('wpo_cache_size_files_to_ignore', $this->files_to_ignore);
		$this->files_to_ignore = is_array($filtered) ? $filtered : array();

		$file = readdir($handle);

		while (false !== $file) {

			if ('.' !== $file && '..' !== $file) {
				$current_file = $dir.'/'.$file;

				if (is_dir($current_file)) {
					$sub_dir_infos = $this->get_dir_infos($current_file);
					$dir_size += $sub_dir_infos['size'];
					$file_count += $sub_dir_infos['file_count'];
				} elseif (is_file($current_file)) {
					if (!in_array($file, $this->files_to_ignore)) {
						$dir_size += filesize($current_file);
						$file_count++;
					}
				}
			}

			$file = readdir($handle);

		}

		return array('size' => $dir_size, 'file_count' => $file_count);
	}

	/**
	 * Returns the path to wp-config
	 *
	 * @return string|boolean wp-config.php path.
	 */
	private function _get_wp_config() {

		$config_path = false;

		foreach (get_included_files() as $filename) {
			if (preg_match('/(\\\\|\/)wp-config\.php$/i', $filename)) {
				$config_path = $filename;
				break;
			}
		}

		// WP-CLI doesn't include wp-config.php that's why we use function from WP-CLI to locate config file.
		if (!$config_path && is_callable('wpo_wp_cli_locate_wp_config')) {
			$config_path = wpo_wp_cli_locate_wp_config();
		}

		return $config_path;
	}

	/**
	 * Util to delete folders and/or files
	 *
	 * @param string $src
	 * @return boolean
	 */
	public static function delete($src) {
		return wpo_delete_files($src);
	}

	/**
	 * Delete cached files for specific url.
	 *
	 * @param string $url
	 * @param bool   $recursive If true child elements will deleted too
	 *
	 * @return bool
	 */
	public static function delete_cache_by_url($url, $recursive = false) {
		if (!defined('WPO_CACHE_FILES_DIR') || '' === $url) return false;

		$path = self::get_full_path_from_url($url);

		do_action('wpo_delete_cache_by_url', $url, $recursive);

		do_action('wpo_preload_url', $url);

		return wpo_delete_files($path, $recursive);
	}

	/**
	 * Adds the URL to the preload list and hooks the preload task to shutdown
	 *
	 * @param string $url
	 * @return void
	 */
	public function maybe_preload_url($url) {
		if (!$this->should_auto_preload_purged_contents()) return;

		static $urls_preloaded = array();

		// Remove the query string from the URL
		$url = preg_replace('/\?.*/', '', $url);

		if (!in_array($url, $urls_preloaded)) {
			$urls_preloaded[] = $url;

			$preloader = WP_Optimize_Page_Cache_Preloader::instance();
			$preloader->add_url_to_preload_list($url);
			$url_task_creator = array($preloader, 'create_tasks_for_auto_preload_urls');

			if (!has_action('shutdown', $url_task_creator)) {
				add_action('shutdown', $url_task_creator);
			}
		}
	}
	
	/**
	 * Delete cached files for single post.
	 *
	 * @param integer $post_id The post ID
	 *
	 * @return bool
	 */
	public static function delete_single_post_cache($post_id) {
		$is_cache_deleted = self::really_delete_single_post_cache($post_id);

		do_action('wpo_single_post_cache_deleted', $post_id);

		return $is_cache_deleted;
	}

	/**
	 * Really delete cached files for single post.
	 *
	 * @param integer $post_id The post ID
	 *
	 * @return bool
	 */
	public static function really_delete_single_post_cache($post_id) {

		if (!defined('WPO_CACHE_FILES_DIR')) return false;

		$post_url = get_permalink($post_id);
	
		$path = self::get_full_path_from_url($post_url);

		// for posts with pagination run purging cache recursively.
		$post = get_post($post_id);
		$recursive = preg_match('/\<\!--nextpage--\>/', $post->post_content) ? true : false;

		do_action('wpo_delete_cache_by_url', $post_url, $recursive);

		do_action('wpo_preload_url', $post_url);

		return wpo_delete_files($path, $recursive);
	}

	/**
	 * Delete cached home page files.
	 */
	public static function delete_homepage_cache() {
	
		if (!defined('WPO_CACHE_FILES_DIR')) return;

		$homepage_url = get_home_url(get_current_blog_id());

		$path = self::get_full_path_from_url($homepage_url);

		$recursive = false;

		do_action('wpo_delete_cache_by_url', $homepage_url, $recursive);

		do_action('wpo_preload_url', $homepage_url);

		wpo_delete_files($path, $recursive);
	}

	/**
	 * Delete feed from cache.
	 */
	public static function delete_feed_cache() {
		if (!defined('WPO_CACHE_FILES_DIR')) return;

		$homepage_url = get_home_url(get_current_blog_id());

		$path = self::get_full_path_from_url($homepage_url) . 'feed/';

		do_action('wpo_delete_cache_by_url', $path, true);

		wpo_delete_files($path, true);
	}

	/**
	 * Delete post feed from cache.
	 *
	 * @param int $post_id Post ID
	 */
	public static function delete_post_feed_cache($post_id) {
		self::really_delete_post_feed_cache($post_id);

		do_action('wpo_single_post_feed_cache_deleted', $post_id);
	}

	/**
	 * Really delete post feed from cache.
	 *
	 * @param int $post_id Post ID
	 */
	public static function really_delete_post_feed_cache($post_id) {
		if (!defined('WPO_CACHE_FILES_DIR')) return;

		$post_url = get_permalink($post_id);
	
		$path = self::get_full_path_from_url($post_url) . 'feed/';

		do_action('wpo_delete_cache_by_url', $path, true);

		wpo_delete_files($path, true);
	}

	/**
	 * Delete comments feed from cache.
	 */
	public static function delete_comments_feed() {
		if (!defined('WPO_CACHE_FILES_DIR')) return;

		$comments_feed_url = trailingslashit(get_home_url(get_current_blog_id())) . 'comments/feed/';

		$path = self::get_full_path_from_url($comments_feed_url);

		do_action('wpo_delete_cache_by_url', $comments_feed_url, true);

		wpo_delete_files($path, true);

		// delete empty comments dir from the cache
		$comments_url = trailingslashit(get_home_url(get_current_blog_id())) . 'comments/';
		$path = self::get_full_path_from_url($comments_url);
		
		if (wpo_is_empty_dir($path)) {
			wpo_delete_files($path, true);
		}
	}

	/**
	 * Returns full path to the cache folder by url.
	 *
	 * @param string $url
	 * @return string
	 */
	public static function get_full_path_from_url($url) {
		return trailingslashit(WPO_CACHE_FILES_DIR) . trailingslashit(wpo_get_url_path($url));
	}

	/**
	 * Admin actions
	 *
	 * @return void
	 */
	public function admin_init() {
		// Maybe update the advanced cache.
		if ((!defined('DOING_AJAX') || !DOING_AJAX) && current_user_can('update_plugins')) {
			$this->maybe_update_advanced_cache();
			$this->cron_activate();
		}
	}

	/**
	 * Logs error messages
	 *
	 * @param  string $message
	 * @return null|void
	 */
	public function log($message) {
		if (isset($this->logger)) {
			$this->logger->log($message, 'error');
		}
	}

	/**
	 * Returns an instance of the current class, creates one if it doesn't exist
	 *
	 * @return self
	 */
	public static function instance() {
		if (empty(self::$instance)) {
			self::$instance = new self();
		}
		return self::$instance;
	}


	/**
	 * Update gmt_offset and timezone_string cache config value, used with hook `update_gmt_offset_timezone_string_config`.
	 *
	 * @param float | string  $old_value GMT offset as a float value or timezone value supported by PHP as a string (https://www.php.net/manual/en/timezones.php)
	 * @param float | string  $new_value
	 * @param string    	  $option    gmt_offset|timezone_string
	 *
	 * @return null
	 */
	public function update_gmt_offset_timezone_string_config($old_value, $new_value, $option) {
		if ('' === $new_value) return;

		$current_config = $this->config->get();
		if ('timezone_string' === $option) {
			$timeZone = new DateTimeZone($new_value);
			$dateTime = new DateTime("now");
			$gmt_offset = $timeZone->getOffset($dateTime);
			$current_config['gmt_offset'] = round($gmt_offset/3600, 1);
			$current_config['timezone_string'] = $new_value;
		} elseif ('' !== $new_value) {
			$current_config['gmt_offset'] = $new_value;
			$current_config['timezone_string'] = '';
		}
		$this->config->update($current_config, true);
	}
	
	/**
	 * Update `date_format` cache config value, used with hook `update_option_date_format`.
	 *
	 * @param string $old_value Old date format
	 * @param string $new_value New date format
	 *
	 * @return void
	 */
	public function update_option_date_format($old_value, $new_value) {
		$current_config = $this->config->get();
		$current_config['date_format'] = $new_value;
		$this->config->update($current_config, true);
	}
	
	/**
	 * Update `time_format` cache config value, used with hook `update_option_time_format`.
	 *
	 * @param string $old_value Old time format
	 * @param string $new_value New time format
	 *
	 * @return void
	 */
	public function update_option_time_format($old_value, $new_value) {
		$current_config = $this->config->get();
		$current_config['time_format'] = $new_value;
		$this->config->update($current_config, true);
	}
 
	/**
	 * Adds an error to the error store
	 *
	 * @param string $code    - The error code
	 * @param string $message - The error's message
	 * @param string $type    - The error's type (error, warning)
	 * @return void
	 */
	public function add_error($code, $message, $type = 'error') {
		if (!isset($this->_errors[$type])) {
			$this->_errors[$type] = new WP_Error($code, $message);
		} else {
			$this->_errors[$type]->add($code, $message);
		}
	}

	/**
	 * Adds a warning to the error store
	 *
	 * @param string $code    - The error code
	 * @param string $message - The error's message
	 * @return void
	 */
	public function add_warning($code, $message) {
		$this->add_error($code, $message, 'warning');
	}

	/**
	 * Get all recorded errors
	 *
	 * @param string  $type              - The error type
	 * @param boolean $get_messages_only - Whether to get only the messages, or the full WP_Error object
	 * @return boolean|array|WP_Error
	 */
	public function get_errors($type = 'error', $get_messages_only = true) {
		if (!$this->has_errors($type)) return false;
		$errors = $this->_errors[$type];
		if ($get_messages_only) {
			return $errors->get_error_messages();
		}
		return $errors;
	}

	/**
	 * Check if any errors were recorded
	 *
	 * @param string $type - The error type
	 * @return boolean
	 */
	public function has_errors($type = 'error') {
		return isset($this->_errors[$type]) && !empty($this->_errors[$type]) && $this->_errors[$type]->has_errors();
	}

	/**
	 * Check if any warnings were recorded
	 *
	 * @return boolean
	 */
	public function has_warnings() {
		return $this->has_errors('warning');
	}

	/**
	 * Check the cache compatibility issues.
	 */
	public function check_compatibility_issues() {
		if (!$this->is_enabled()) return;

		if ($this->is_pagespeedninja_gzip_active()) add_action('admin_notices', array($this, 'show_pagespeedninja_gzip_notice'));
		if ($this->is_farfutureexpiration_gzip_active()) add_action('admin_notices', array($this, 'show_farfutureexpiration_gzip_notice'));
	}

	/**
	 * Check if PageSpeed Ninja is active and GZIP compression option is enabled.
	 *
	 * @return bool
	 */
	public function is_pagespeedninja_gzip_active() {
		if (!class_exists('PagespeedNinja')) return false;

		$options = get_option('pagespeedninja_config');
		$gzip = isset($options['psi_EnableGzipCompression']) && isset($options['html_gzip']) && (bool) $options['psi_EnableGzipCompression'] && (bool) $options['html_gzip'];

		return $gzip;
	}

	/**
	 * Output PageSpeed Ninja Gzip notice.
	 */
	public function show_pagespeedninja_gzip_notice() {
		echo '<div id="wp-optimize-pagespeedninja-gzip-notice" class="error wpo-notice"><p><b>'.esc_html__('WP-Optimize:', 'wp-optimize').'</b> '.esc_html__('Please disable the feature "Gzip compression" in PageSpeed Ninja to prevent conflicts.', 'wp-optimize').'</p></div>';
	}

	/**
	 * Check if Far Future Expiration is active and GZIP compression option is enabled.
	 *
	 * @return bool
	 */
	public function is_farfutureexpiration_gzip_active() {
		if (!class_exists('farFutureExpiration')) return false;

		$options = get_option('far_future_expiration_settings');
		$gzip = !empty($options) && $options['enable_gzip'];

		return $gzip;
	}

	/**
	 * Output Far Future Expiration Gzip notice.
	 */
	public function show_farfutureexpiration_gzip_notice() {
		echo '<div id="wp-optimize-pagespeedninja-gzip-notice" class="error wpo-notice"><p><b>'.esc_html__('WP-Optimize:', 'wp-optimize').'</b> '.esc_html__('Please disable the feature "Gzip compression" in Far Future Expiration to prevent conflicts.', 'wp-optimize').'</p></div>';
	}

	/**
	 * This is a notice to show users that writing `advanced-cache.php` failed
	 */
	public function show_admin_notice_advanced_cache() {
		// translators: %s is advanced-cache.php file path
		$message = sprintf(__('The request to write the file %s failed.', 'wp-optimize'), htmlspecialchars($this->get_advanced_cache_filename()));
		$message .= ' '.__('Please check file and directory permissions on the file paths up to this point, and your PHP error log.', 'wp-optimize');
		WP_Optimize()->include_template('notices/cache-notice.php', false, array('message' => $message));
	}

	/**
	 * Scheduler public functions to update schedulers
	 *
	 * @param  array $schedules An array of schedules being passed.
	 * @return array            An array of schedules being returned.
	 */
	public function cron_schedules($schedules) {
		$page_cache_length = $this->config->get_option('page_cache_length');
		// When the interval is less than `WP_CRON_LOCK_TIMEOUT` error messages are triggered
		// So, instead of `0` (now) we are making it equal to the constant value.
		// https://github.com/WordPress/WordPress/commit/84d19848666a81584e0007a6ab7f7ad3c990d71a
		if (defined('WP_CRON_LOCK_TIMEOUT') && $page_cache_length < WP_CRON_LOCK_TIMEOUT) {
			$page_cache_length = WP_CRON_LOCK_TIMEOUT;
		}

		$schedules['wpo_purge_old_cache'] = array('interval' => false === $page_cache_length ? 86400 : $page_cache_length, 'display' => __('Every time after the cache has expired', 'wp-optimize'));

		$interval = WP_Optimize_Page_Cache_Preloader::instance()->get_continue_preload_cron_interval();
		$schedules['wpo_page_cache_preload_continue_interval'] = array(
			'interval' => $interval,
			// translators: %d is the number of minutes
			'display' => sprintf(__('%d minutes', 'wp-optimize'), round($interval / 60, 1))
		);

		$schedules['wpo_use_cache_lifespan'] = array(
			'interval' => $page_cache_length,
			// translators: %s is the cache lifespan
			'display' => sprintf(__('Same as cache lifespan: %s', 'wp-optimize'), WPO_Cache_Config::instance()->get_option('page_cache_length_value').' '.WPO_Cache_Config::instance()->get_option('page_cache_length_unit'))
		);

		return $schedules;
	}

	/**
	 * Update cache config file to reflect the webp images option
	 */
	public function update_webp_images_option() {
		if ($this->is_enabled()) {
			$cache_settings = WPO_Cache_Config::instance()->get();
			$cache_settings['use_webp_images'] = WP_Optimize()->get_options()->get_option('webp_conversion');
			WPO_Cache_Config::instance()->update($cache_settings);
		}
	}

	/**
	 * Maybe regenerate cache config file, in case of migrations
	 */
	public function maybe_regenerate_cache_config_file() {
		$config_file = WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename();
		if (!file_exists($config_file)) {
			$config = $this->config->get();
			$this->config->write($config);
		}
	}
	
	/**
	 * Checks if the given cache directory is empty
	 *
	 * @param string $path The path to the cache directory.
	 * @return bool Returns true if the cache directory is empty, false otherwise.
	 */
	public static function is_cache_empty($path) {
		if (!is_dir($path)) return true;
		
		if ($handle = opendir($path)) {
			while (false !== ($entry = readdir($handle))) {
				if ("." === $entry || ".." === $entry) {
					continue;
				}
				
				$full_path = $path . '/' . $entry;
				if (is_file($full_path)) {
					closedir($handle);
					return false;
				}
			}
			closedir($handle);
			return true;
		}
		return true;
	}

	/**
	 * Remove query params from request url
	 *
	 * @param string $url The request url.
	 * @return string Returns url without query params.
	 */
	private static function remove_query_params($url) {
		$parsed_url = wp_parse_url($url);
		if (!$parsed_url) return '';
		return $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'];
	}

	/**
	 * Check configuration for auto preloading after cache deletion
	 *
	 * @return bool
	 */
	public function should_auto_preload_purged_contents() {

		if (!$this->is_enabled()) return false; // as Page Cache is not enabled we should not auto preload purged content.

		$wpo_cache = WP_Optimize()->get_page_cache();
		$wpo_cache_options = $wpo_cache->config->get();

		return $wpo_cache_options['auto_preload_purged_contents'];
	}

	/**
	 * Logging of interesting messages related to Cache Purging
	 *
	 * @param string $message
	 * @param int $post_id
	 */
	public function file_log($message, $post_id = 0) {
		if (!empty($post_id)) {
			if (false !== strpos($message, '{{URL}}')) {
				$message = str_replace('{{URL}}', get_permalink($post_id), $message);
			}
			if (false !== strpos($message, '{{title}}')) {
				$message = str_replace('{{title}}', '"' . get_the_title($post_id) . '"', $message);
			}
		}
		$this->file_logger->info($message);
	}

	/**
	 * Prunes the log file
	 */
	public function prune_cache_logs() {
		$this->file_log("Pruning the Cache log file");
		$this->file_logger->prune_logs();
	}

	/**
	 * Update .htaccess file to serve cached files directly.
	 *
	 * @param bool $enable
	 */
	public function update_serve_cache_rules_htaccess_section($enable) {
		$htaccess = new WP_Optimize_Htaccess();

		$section_title = 'WP-Optimize rules to serve cached files';

		if ($enable) {
			if (!$htaccess->is_commented_section_exists($section_title)) {
				$htaccess->update_commented_section($this->prepare_apache_cache_handle_section(), $section_title, true);
				$htaccess->write_file();
			}
		} elseif ($htaccess->is_commented_section_exists($section_title)) {
			$htaccess->remove_commented_section($section_title);
			$htaccess->write_file();
		}

	}

	/**
	 * Get the .htaccess section with rules to serve cached files
	 *
	 * @return array
	 */
	private function prepare_apache_cache_handle_section() {

		$site_root = $this->get_site_root_path();

		return array(
			array(
				'<IfModule mod_rewrite.c>',
				'RewriteEngine On',
				'RewriteCond %{REQUEST_METHOD} GET|HEAD',
				'RewriteCond %{QUERY_STRING} =""',
				'RewriteCond %{HTTP:Cookie} =""',
				'RewriteCond %{REQUEST_URI} !^/(wp-(?:admin|login|register|comments-post|cron|json))/ [NC]',
				'RewriteCond %{HTTP_USER_AGENT} !(android|blackberry|ipad|iphone|ipod|windows.phone|iEMobile|opera.mini|opera.mobile|webos|symbian|windows.mobile|kindle|googlebot.mobile) [NC]',
				'RewriteCond %{DOCUMENT_ROOT}'.$site_root.'wp-content/cache/wpo-cache/%{HTTP_HOST}%{REQUEST_URI}index.html -f',
				'RewriteRule ^(.*)$ '.$site_root.'wp-content/cache/wpo-cache/%{HTTP_HOST}%{REQUEST_URI}index.html [L]',
				array(
					'<IfModule mod_setenvif.c>',
					'SetEnvIf Request_URI "/wp-content/cache/wpo-cache/" FROM_CACHE=1',
					'</IfModule>',
				),
				array(
					'<IfModule mod_headers.c>',
					'Header always set X-WP-Optimize-Cache-Header "Loaded from WP-Optimize cache" env=FROM_CACHE',
					'</IfModule>',
				),
				'</IfModule>',
			),
		);
	}

	/**
	 * Get relative site root path, if site is placed in the subdirectory then it returns /subdirectory otherwise /
	 *
	 * @param string $default
	 *
	 * @return string
	 */
	private function get_site_root_path($default = '/') {
		$site_root = wp_parse_url(site_url());
		if (isset($site_root['path'])) {
			$site_root = trailingslashit($site_root['path']);
		} else {
			$site_root = $default;
		}

		return $site_root;
	}
}

endif;
